// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integration

import (
	"math"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	"github.com/FerretDB/FerretDB/v2/integration/setup"
	"github.com/FerretDB/FerretDB/v2/integration/shareddata"
)

func TestUpdateFieldSet(t *testing.T) {
	t.Parallel()

	for name, tc := range map[string]struct {
		id     string // optional, defaults to empty
		update bson.D // required, used for update parameter

		res     *mongo.UpdateResult // optional, expected response from update
		findRes bson.D              // optional, expected response from find
	}{
		"ArrayNil": {
			id:      "string",
			update:  bson.D{{"$set", bson.D{{"v", bson.A{nil}}}}},
			findRes: bson.D{{"_id", "string"}, {"v", bson.A{nil}}},
			res: &mongo.UpdateResult{
				MatchedCount:  1,
				ModifiedCount: 1,
				UpsertedCount: 0,
			},
		},
		"SetSameValueInt": {
			id:      "int32",
			update:  bson.D{{"$set", bson.D{{"v", int32(42)}}}},
			findRes: bson.D{{"_id", "int32"}, {"v", int32(42)}},
			res: &mongo.UpdateResult{
				MatchedCount:  1,
				ModifiedCount: 0,
				UpsertedCount: 0,
			},
		},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			require.NotNil(t, tc.update, "update should be set")

			ctx, collection := setup.Setup(t, shareddata.Scalars, shareddata.Composites)

			res, err := collection.UpdateOne(ctx, bson.D{{"_id", tc.id}}, tc.update)

			require.NoError(t, err)
			require.Equal(t, tc.res, res)

			var actual bson.D
			err = collection.FindOne(ctx, bson.D{{"_id", tc.id}}).Decode(&actual)
			require.NoError(t, err)
			AssertEqualDocuments(t, tc.findRes, actual)
		})
	}
}

// TestUpdateFieldSetIDDoc checks that the order of fields in the _id document matters.
func TestUpdateFieldSetIDDoc(t *testing.T) {
	t.Parallel()

	ctx, collection := setup.Setup(t)

	_, err := collection.InsertOne(ctx, bson.D{
		{"_id", bson.D{{"a", int32(1)}, {"z", int32(2)}}},
		{"v", int32(1)},
	})
	require.NoError(t, err)
	_, err = collection.InsertOne(ctx, bson.D{
		{"_id", bson.D{{"a", int32(3)}, {"z", int32(4)}}},
		{"v", int32(2)},
	})
	require.NoError(t, err)

	res, err := collection.UpdateByID(
		ctx,
		bson.D{{"a", int32(3)}, {"z", int32(4)}},
		bson.D{{"$set", bson.D{{"v", int32(3)}}}},
	)
	require.NoError(t, err)
	assert.Equal(t, &mongo.UpdateResult{MatchedCount: 1, ModifiedCount: 1}, res)

	expected := []bson.D{{
		{"_id", bson.D{{"a", int32(1)}, {"z", int32(2)}}},
		{"v", int32(1)},
	}, {
		{"_id", bson.D{{"a", int32(3)}, {"z", int32(4)}}},
		{"v", int32(3)},
	}}
	AssertEqualDocumentsSlice(t, expected, FindAll(t, ctx, collection))

	res, err = collection.UpdateByID(
		ctx,
		bson.D{{"z", int32(4)}, {"a", int32(3)}},
		bson.D{{"$set", bson.D{{"v", int32(4)}}}},
	)
	require.NoError(t, err)
	assert.Equal(t, new(mongo.UpdateResult), res)

	AssertEqualDocumentsSlice(t, expected, FindAll(t, ctx, collection))
}

func TestUpdateFieldSetUpdateManyUpsert(t *testing.T) {
	t.Parallel()

	for name, tc := range map[string]struct { //nolint:vet // used for testing only
		filter bson.D                 // optional, defaults to bson.D{}
		update bson.D                 // required, used for update parameter
		opts   *options.UpdateOptions // optional

		findRes []bson.E // required, expected response from find without _id generated by upsert
		skip    string   // optional, skip test with a specified reason
	}{
		"QueryOperator": {
			filter:  bson.D{{"v", bson.D{{"$lt", 3}}}},
			update:  bson.D{{"$set", bson.D{{"new", "val"}}}},
			opts:    options.Update().SetUpsert(true),
			findRes: []bson.E{{"new", "val"}},
		},
		"NoQueryOperator": {
			filter:  bson.D{{"v", int32(4080)}},
			update:  bson.D{{"$set", bson.D{{"new", "val"}}}},
			opts:    options.Update().SetUpsert(true),
			findRes: []bson.E{{"v", int32(4080)}, {"new", "val"}},
		},
		"QueryOperatorIDQuery": {
			filter:  bson.D{{"_id", bson.D{{"$eq", 1}}}},
			update:  bson.D{{"$set", bson.D{{"new", "val"}}}},
			opts:    options.Update().SetUpsert(true),
			findRes: []bson.E{{"new", "val"}},
		},
	} {
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			require.NotNil(t, tc.update, "update should be set")
			require.NotNil(t, tc.findRes, "findRes should be set")

			ctx, collection := setup.Setup(t, shareddata.Nulls)

			updateRes, err := collection.UpdateMany(ctx, tc.filter, tc.update, tc.opts)
			require.NoError(t, err)
			assert.Equal(t, int64(0), updateRes.MatchedCount)
			assert.Equal(t, int64(0), updateRes.ModifiedCount)
			assert.Equal(t, int64(1), updateRes.UpsertedCount)
			require.NotNil(t, updateRes.UpsertedID)

			cursor, err := collection.Find(ctx, bson.D{{"_id", updateRes.UpsertedID}})
			require.NoError(t, err)

			var res []bson.D
			err = cursor.All(ctx, &res)
			require.NoError(t, err)

			expected := bson.D{{"_id", updateRes.UpsertedID}}
			for _, e := range tc.findRes {
				expected = append(expected, e)
			}

			AssertEqualDocumentsSlice(t, []bson.D{expected}, res)
		})
	}
}

func TestUpdateCommandUpsert(tt *testing.T) {
	tt.Parallel()

	for name, tc := range map[string]struct { //nolint:vet // used for testing only
		updates bson.A // required, used for update parameter

		updateRes bson.D
		findRes   []bson.D          // required, expected response from find without _id generated by upsert
		err       *mongo.WriteError // optional, expected error from MongoDB

		skip             string // optional, skip test with a specified reason
		failsForFerretDB string
	}{
		"NoUpdateOperator": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{{"v", bson.D{{"$lt", 3}}}}},
					{"u", bson.D{{"updateV", "val"}}},
					{"upsert", true},
				},
			},
			updateRes: bson.D{
				{"n", int32(1)},
				{"upserted", bson.A{
					bson.D{{"index", int32(0)}, {"_id", primitive.ObjectID{}}},
				}},
				{"nModified", int32(0)},
				{"ok", float64(1)},
			},

			findRes: []bson.D{{{"_id", ""}, {"v", nil}}, {{"_id", primitive.ObjectID{}}, {"updateV", "val"}}},
		},
		"NoUpdateOperatorEmptyQuery": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{}},
					{"u", bson.D{{"updateV", "val"}}},
					{"upsert", true},
				},
			},
			updateRes: bson.D{
				{"n", int32(1)},
				{"nModified", int32(1)},
				{"ok", float64(1)},
			},
			findRes: []bson.D{{{"_id", ""}, {"updateV", "val"}}},
		},
		"NoQueryOperator": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{{"queryV", "v"}}},
					{"u", bson.D{{"$set", bson.D{{"updateV", "val"}}}}},
					{"upsert", true},
				},
			},
			updateRes: bson.D{
				{"n", int32(1)},
				{"upserted", bson.A{
					bson.D{{"index", int32(0)}, {"_id", primitive.ObjectID{}}},
				}},
				{"nModified", int32(0)},
				{"ok", float64(1)},
			},
			findRes: []bson.D{{{"_id", ""}, {"v", nil}}, {{"_id", primitive.ObjectID{}}, {"queryV", "v"}, {"updateV", "val"}}},
		},
		"NoUpdateOperatorNoQueryOperator": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{{"queryV", "val"}}},
					{"u", bson.D{{"updateV", "val"}}},
					{"upsert", true},
				},
			},
			updateRes: bson.D{
				{"n", int32(1)},
				{"upserted", bson.A{
					bson.D{{"index", int32(0)}, {"_id", primitive.ObjectID{}}},
				}},
				{"nModified", int32(0)},
				{"ok", float64(1)},
			},
			findRes: []bson.D{{{"_id", ""}, {"v", nil}}, {{"_id", primitive.ObjectID{}}, {"updateV", "val"}}},
		},
		"MultipleUpserts": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{{"v", bson.D{{"$gt", 3}}}}},
					{"u", bson.D{{"updateV", "greater"}}},
					{"upsert", true},
				},
				bson.D{
					{"q", bson.D{{"v", bson.D{{"$lt", 3}}}}},
					{"u", bson.D{{"updateV", "less"}}},
					{"upsert", true},
				},
			},
			updateRes: bson.D{
				{"n", int32(2)},
				{"upserted", bson.A{
					bson.D{{"index", int32(0)}, {"_id", primitive.ObjectID{}}},
					bson.D{{"index", int32(1)}, {"_id", primitive.ObjectID{}}},
				}},
				{"nModified", int32(0)},
				{"ok", float64(1)},
			},
			findRes: []bson.D{
				{{"_id", ""}, {"v", nil}},
				{{"_id", primitive.ObjectID{}}, {"updateV", "greater"}},
				{{"_id", primitive.ObjectID{}}, {"updateV", "less"}},
			},
		},
		"UnknownUpdateOperator": {
			updates: bson.A{
				bson.D{
					{"q", bson.D{{"v", bson.D{{"$lt", 3}}}}},
					{"u", bson.D{{"$unknown", bson.D{{"v", "val"}}}}},
					{"upsert", true},
				},
			},
			err: &mongo.WriteError{
				Code:    9,
				Message: "Unknown modifier: $unknown. Expected a valid update modifier or pipeline-style update specified as an array",
			},
		},
	} {
		tt.Run(name, func(tt *testing.T) {
			tt.Parallel()

			require.NotNil(tt, tc.updates, "update should be set")

			ctx, collection := setup.Setup(tt, shareddata.Nulls)

			var t testing.TB = tt

			if tc.failsForFerretDB != "" {
				t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
			}

			var res bson.D
			err := collection.Database().RunCommand(ctx,
				bson.D{
					{"update", collection.Name()},
					{"updates", tc.updates},
				}).Decode(&res)

			if tc.err != nil {
				assert.Nil(t, res)
				AssertEqualWriteError(t, *tc.err, err)

				return
			}

			var actualComparable bson.D

			for _, field := range res {
				switch field.Key {
				case "upserted":
					require.IsType(t, bson.A{}, field.Value)
					upserted := field.Value.(bson.A)

					var actualComparableUpserted bson.A

					for _, field := range upserted {
						var actualElem bson.D

						require.IsType(t, bson.D{}, field)
						upsertedElem := field.(bson.D)

						for _, field := range upsertedElem {
							switch field.Key {
							case "_id":
								// _id is generated, cannot check for exact value so check it is not zero value
								assert.IsType(t, primitive.ObjectID{}, field.Value)
								actualElem = append(actualElem, bson.E{"_id", primitive.ObjectID{}})
							default:
								actualElem = append(actualElem, field)
							}
						}

						actualComparableUpserted = append(actualComparableUpserted, actualElem)
					}

					actualComparable = append(actualComparable, bson.E{"upserted", actualComparableUpserted})

				default:
					actualComparable = append(actualComparable, field)
				}
			}

			AssertEqualDocuments(t, tc.updateRes, actualComparable)

			cursor, err := collection.Find(ctx, bson.D{}, options.Find().SetSort(bson.D{{"v", 1}, {"_id", 1}}))
			require.NoError(t, err)

			var findRes []bson.D
			err = cursor.All(ctx, &findRes)
			require.NoError(t, err)

			var actualComparableFind []bson.D

			for _, doc := range findRes {
				var comparableDoc bson.D

				for _, elem := range doc {
					switch elem.Key {
					case "_id":
						switch elem.Value.(type) {
						case string:
							comparableDoc = append(comparableDoc, bson.E{"_id", ""})
						case primitive.ObjectID:
							comparableDoc = append(comparableDoc, bson.E{"_id", primitive.ObjectID{}})
						default:
							require.Fail(tt, "The _id field should be of a string type or primitive.ObjectID")
						}
					default:
						comparableDoc = append(comparableDoc, elem)
					}
				}

				actualComparableFind = append(actualComparableFind, comparableDoc)
			}

			AssertEqualDocumentsSlice(t, tc.findRes, actualComparableFind)
		})
	}
}

func TestUpdateFieldErrors(t *testing.T) {
	t.Parallel()

	for name, tc := range map[string]struct { //nolint:vet // it is used for test only
		id       string              // optional, defaults to empty
		update   bson.D              // required, used for update parameter
		provider shareddata.Provider // optional, default uses shareddata.ArrayDocuments

		err              *mongo.WriteError // required, expected error from MongoDB
		altMessage       string            // optional, alternative error message for FerretDB, ignored if empty
		failsForFerretDB string
	}{
		"SetUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$rename", bson.D{{"v.foo", "foo"}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "cannot use the part (v of v.foo) to traverse the element " +
					"({v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]})",
			},
			altMessage:       "cannot use path 'v.foo' to traverse the document",
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"SetImmutableID": {
			id:     "array-documents-nested",
			update: bson.D{{"$set", bson.D{{"_id", "another-id"}}}},
			err: &mongo.WriteError{
				Code:    66,
				Message: "Performing an update on the path '_id' would modify the immutable field '_id'",
			},
		},
		"RenameEmptyFieldName": {
			id:     "array-documents-nested",
			update: bson.D{{"$rename", bson.D{{"", "v"}}}},
			err: &mongo.WriteError{
				Code:    56,
				Message: "An empty update path is not valid.",
			},
			altMessage: "An empty update path is not valid",
		},
		"RenameEmptyPath": {
			id:     "array-documents-nested",
			update: bson.D{{"$rename", bson.D{{"v.", "v"}}}},
			err: &mongo.WriteError{
				Code:    56,
				Message: "The update path 'v.' contains an empty field name, which is not allowed.",
			},
			altMessage: "An update path 'v.' contains an empty field name, which is not allowed.",
		},
		"RenameArrayInvalidIndex": {
			id:     "array-documents-nested",
			update: bson.D{{"$rename", bson.D{{"v.-1", "f"}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "cannot use the part (v of v.-1) to traverse the element " +
					"({v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]})",
			},
			altMessage:       "cannot use path 'v.-1' to traverse the document",
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"RenameUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$rename", bson.D{{"v.0.foo.0.bar.z", "f"}}}},
			err: &mongo.WriteError{
				Code:    28,
				Message: "cannot use the part (bar of v.0.foo.0.bar.z) to traverse the element ({bar: \"hello\"})",
			},
			altMessage: "Cannot create field 'bar' in element {bar : \"hello\"}",
		},
		"IncTypeMismatch": {
			id:     "array-documents-nested",
			update: bson.D{{"$inc", bson.D{{"v", "string"}}}},
			err: &mongo.WriteError{
				Code:    14,
				Message: "Cannot increment with non-numeric argument: {v: \"string\"}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/431",
		},
		"IncUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$inc", bson.D{{"v.foo", 1}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "Cannot create field 'foo' in element " +
					"{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"IncNonNumeric": {
			id:     "array-documents-nested",
			update: bson.D{{"$inc", bson.D{{"v.0.foo.0.bar", 1}}}},
			err: &mongo.WriteError{
				Code: 14,
				Message: "Cannot apply $inc to a value of non-numeric type. " +
					"{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string",
			},
			altMessage: "Cannot apply $inc to a value of non-numeric type. { _id: \"array-documents-nested\" } " +
				"has the field 'bar' of non-numeric type string",
		},
		"IncInt64BadValue": {
			id:     "int64-max",
			update: bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "Failed to apply $inc operations to current value " +
					"((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}",
			},
			provider: shareddata.Int64s,
		},
		"IncInt32BadValue": {
			id:     "int32",
			update: bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "Failed to apply $inc operations to current value " +
					"((NumberInt)42) for document {_id: \"int32\"}",
			},
			provider: shareddata.Int32s,
		},
		"MaxUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$max", bson.D{{"v.foo", 1}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "Cannot create field 'foo' in element " +
					"{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"MinUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$min", bson.D{{"v.foo", 1}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "Cannot create field 'foo' in element " +
					"{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"MulTypeMismatch": {
			id:     "array-documents-nested",
			update: bson.D{{"$mul", bson.D{{"v", "string"}}}},
			err: &mongo.WriteError{
				Code:    14,
				Message: "Cannot multiply with non-numeric argument: {v: \"string\"}",
			},
			altMessage: "Cannot multiply with non-numeric argument: { v : \"string\" }",
		},
		"MulTypeMismatchNonExistent": {
			id:     "array-documents-nested",
			update: bson.D{{"$mul", bson.D{{"non-existent", "string"}}}},
			err: &mongo.WriteError{
				Code:    14,
				Message: "Cannot multiply with non-numeric argument: {non-existent: \"string\"}",
			},
			altMessage: "Cannot multiply with non-numeric argument: { non-existent : \"string\" }",
		},
		"MulUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$mul", bson.D{{"v.foo", 1}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "Cannot create field 'foo' in element " +
					"{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"MulNonNumeric": {
			id:     "array-documents-nested",
			update: bson.D{{"$mul", bson.D{{"v.0.foo.0.bar", 1}}}},
			err: &mongo.WriteError{
				Code: 14,
				Message: "Cannot apply $mul to a value of non-numeric type. " +
					"{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string",
			},
			altMessage: "Cannot apply $mul to a value of non-numeric type. { _id: \"array-documents-nested\" } " +
				"has the field 'bar' of non-numeric type string",
		},
		"MulInt64BadValue": {
			id:     "int64-max",
			update: bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "Failed to apply $mul operations to current value " +
					"((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}",
			},
			provider:   shareddata.Int64s,
			altMessage: "Failed to apply $mul operations to current ((NumberLong)9223372036854775807) value for document { _id: \"int64-max\" }",
		},
		"MulInt32BadValue": {
			id:     "int32",
			update: bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "Failed to apply $mul operations to current value " +
					"((NumberInt)42) for document {_id: \"int32\"}",
			},
			provider:   shareddata.Int32s,
			altMessage: "Failed to apply $mul operations to current ((NumberInt)42) value for document { _id: \"int32\" }",
		},
		"MulEmptyPath": {
			id:     "array-documents-nested",
			update: bson.D{{"$mul", bson.D{{"v.", "v"}}}},
			err: &mongo.WriteError{
				Code:    56,
				Message: "The update path 'v.' contains an empty field name, which is not allowed.",
			},
			altMessage: "An update path 'v.' contains an empty field name, which is not allowed.",
		},
		"BitUnsupportedOperator": {
			id: "int32",
			update: bson.D{
				{"$bit", bson.D{{"v", bson.D{{"not", 1}}}}},
			},
			err: &mongo.WriteError{
				Code: 2,
				Message: "The $bit modifier only supports 'and', 'or', and 'xor', not 'not' " +
					"which is an unknown operator: {not: 1}",
			},
			provider:   shareddata.Int32s,
			altMessage: "The $bit modifier only supports 'and', 'or', and 'xor', not 'not' which is an unknown operator: { \"not\" : 1 }",
		},
		"BitEmptyFieldPath": {
			id:     "array-documents-nested",
			update: bson.D{{"$bit", bson.D{{"v.", bson.D{{"and", 1}}}}}},
			err: &mongo.WriteError{
				Code:    56,
				Message: "The update path 'v.' contains an empty field name, which is not allowed.",
			},
			altMessage: "An update path 'v.' contains an empty field name, which is not allowed.",
		},
		"BitEmptyOperation": {
			id:     "array-documents-nested",
			update: bson.D{{"$bit", bson.D{{"v", bson.D{}}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "You must pass in at least one bitwise operation. " +
					"The format is: {$bit: {field: {and/or/xor: #}}",
			},
		},
		"BitUnsuitableValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$bit", bson.D{{"v.foo", bson.D{{"or", 1}}}}}},
			err: &mongo.WriteError{
				Code: 28,
				Message: "Cannot create field 'foo' in element " +
					"{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}",
			},
			failsForFerretDB: "https://github.com/FerretDB/FerretDB-DocumentDB/issues/429",
		},
		"BitNonIntegralDocValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$bit", bson.D{{"v.0.foo", bson.D{{"and", 1}}}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "Cannot apply $bit to a value of non-integral type." +
					"_id: \"array-documents-nested\" has the field foo of non-integer type array",
			},
			altMessage: "Cannot apply $bit to a value of non-integral type.{ \"_id\" : \"array-documents-nested\" } " +
				"has the field foo of non-integer type array",
		},
		"BitIncompatibleOperatorValue": {
			id:     "array-documents-nested",
			update: bson.D{{"$bit", "string"}},
			err: &mongo.WriteError{
				Code: 9,
				Message: "Modifiers operate on fields but we found type string instead. " +
					"For example: {$mod: {<field>: ...}} not {$bit: \"string\"}",
			},
		},
		"BitEmbeddedDocBadValue": {
			id:     "int32",
			update: bson.D{{"$bit", bson.D{{"test", "and"}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "The $bit modifier is not compatible with a string. " +
					"You must pass in an embedded document: {$bit: {field: {and/or/xor: #}}",
			},
			provider:   shareddata.Int32s,
			altMessage: "$bit should be a document",
		},
		"BitNonIntegralOperand": {
			id:     "int32",
			update: bson.D{{"$bit", bson.D{{"v", bson.D{{"and", "test"}}}}}},
			err: &mongo.WriteError{
				Code: 2,
				Message: "The $bit modifier field must be an Integer(32/64 bit); " +
					"a 'string' is not supported here: {and: \"test\"}",
			},
			provider:   shareddata.Int32s,
			altMessage: "The $bit modifier field must be an Integer(32/64 bit); a 'string' is not supported here: { \"and\" : \"test\" }",
		},
	} {
		t.Run(name, func(tt *testing.T) {
			tt.Parallel()

			var t testing.TB = tt
			if tc.failsForFerretDB != "" {
				t = setup.FailsForFerretDB(tt, tc.failsForFerretDB)
			}

			require.NotNil(t, tc.update, "update should be set")
			require.NotNil(t, tc.err, "err should be set")

			provider := tc.provider
			if provider == nil {
				provider = shareddata.ArrayDocuments
			}

			ctx, collection := setup.Setup(tt, provider)

			res, err := collection.UpdateOne(ctx, bson.D{{"_id", tc.id}}, tc.update)

			assert.Nil(t, res)
			AssertEqualAltWriteError(t, *tc.err, tc.altMessage, err)
		})
	}
}
